﻿using System;
using System.Configuration;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net.NetworkInformation;
using System.Threading;
using FlickrNet;
using JelleDruyts.Windows;
using Schedulr.Models;

namespace Schedulr.Infrastructure
{
    public class FlickrClient
    {
        #region Constants

        private const string ApiKey = "c296ee49a0e9a56020a6f7ec77891d23";
        private const string SharedSecret = "341dd12cf0ed29d5";

        #endregion

        #region Fields

        private Flickr flickr;
        private string frob;
        private object uploadingLock = new object();
        private Action<long> bytesUploadedCallback;
        private static TimeSpan uploadTimeout;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes the <see cref="FlickrClient"/> class.
        /// </summary>
        static FlickrClient()
        {
            var uploadTimeoutSecondsOverrideValue = ConfigurationManager.AppSettings["UploadTimeoutSeconds"];
            int uploadTimeoutSeconds;
            if (!int.TryParse(uploadTimeoutSecondsOverrideValue, out uploadTimeoutSeconds))
            {
                uploadTimeoutSeconds = 600;
            }
            FlickrClient.uploadTimeout = TimeSpan.FromSeconds(uploadTimeoutSeconds);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="FlickrClient"/> class.
        /// </summary>
        public FlickrClient()
            : this(null)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="FlickrClient"/> class.
        /// </summary>
        /// <param name="token">The token.</param>
        public FlickrClient(string token)
        {
            this.flickr = new Flickr(ApiKey, SharedSecret, token);
            this.flickr.OnUploadProgress += OnUploadProgress;
        }

        #endregion

        #region IsOnline

        /// <summary>
        /// Gets a value that determines if Flickr is available.
        /// </summary>
        /// <returns><see langword="true"/> if a network connection is available, <see langword="false"/> otherwise.</returns>
        public static bool IsOnline()
        {
            return NetworkInterface.GetIsNetworkAvailable();
        }

        #endregion

        #region Authentication

        /// <summary>
        /// Begins the authentication process with Flickr and returns the user's authentication URL.
        /// </summary>
        /// <returns>The URL where the user should go next to authenticate online.</returns>
        /// <remarks>
        /// A so-called 'frob' will be requested and stored, and when the user has authenticated
        /// with Flickr through the returned authentication URL, call <see cref="EndAuthentication"/>
        /// to complete the process.
        /// </remarks>
        public string BeginAuthentication()
        {
            this.frob = flickr.AuthGetFrob();
            return flickr.AuthCalcUrl(frob, AuthLevel.Write);
        }

        /// <summary>
        /// Ends the authentication process with Flickr and returns the user's authentication token.
        /// </summary>
        /// <returns>The user's authentication token.</returns>
        /// <remarks>
        /// The 'frob' will be used to verify that the user has now authenticated with Flickr.
        /// </remarks>
        public string EndAuthentication()
        {
            if (string.IsNullOrEmpty(this.frob))
            {
                throw new InvalidOperationException("Cannot end the authentication process because there is no so-called 'frob' that is created as the first step of the authentication process. Call BeginAuthentication first.");
            }
            Auth authentication = this.flickr.AuthGetToken(frob);
            this.flickr.AuthToken = authentication.Token;
            this.frob = null;
            return authentication.Token;
        }

        #endregion

        #region User Info

        /// <summary>
        /// Gets general information about the user.
        /// </summary>
        /// <returns>General information about the user.</returns>
        public UserInfo GetUserInfo()
        {
            // Retrieve information.
            var authentication = this.flickr.AuthCheckToken(this.flickr.AuthToken);
            var user = this.flickr.PeopleGetInfo(authentication.User.UserId);
            var status = this.flickr.PeopleGetUploadStatus();
            var groups = this.flickr.GroupsPoolsGetGroups();
            var sets = this.flickr.PhotosetsGetList();

            // Populate user information.
            var userInfo = new UserInfo();
            userInfo.BuddyIconUrl = user.BuddyIconUrl.ToString();
            userInfo.IsProUser = status.IsPro;
            userInfo.PicturesUrl = user.PhotosUrl.ToString();
            userInfo.UploadLimit = status.FileSizeMax;
            userInfo.UploadLimitVideo = status.VideoSizeMax;
            userInfo.UserName = user.UserName;
            userInfo.UserId = user.UserId;
            foreach (var set in sets)
            {
                userInfo.Sets.Add(new PictureCollection { Id = set.PhotosetId, ImageUrl = set.PhotosetSquareThumbnailUrl.ToString(), Name = set.Title });
            }
            foreach (var group in groups)
            {
                userInfo.Groups.Add(new PictureCollection { Id = group.GroupId, ImageUrl = group.GroupIconUrl.ToString(), Name = group.GroupName });
            }
            return userInfo;
        }

        /// <summary>
        /// Gets the current status information about the user.
        /// </summary>
        /// <returns>The current status information about the user.</returns>
        public FlickrUserStatusInfo GetUserStatus()
        {
            var status = flickr.PeopleGetUploadStatus();
            return new FlickrUserStatusInfo(status.BandwidthUsed, status.BandwidthMax, status.PercentageUsed, status.VideosUploaded, status.VideosRemaining);
        }

        #endregion

        #region Upload

        /// <summary>
        /// Uploads the given picture.
        /// </summary>
        /// <param name="uploadingPicturesTask">The task that tracks uploading pictures.</param>
        /// <param name="account">The account.</param>
        /// <param name="picture">The picture to upload.</param>
        /// <param name="bytesUploadedCallback">The method that is called when the number of uploaded bytes changed.</param>
        /// <returns><see langword="true"/> if the picture was uploaded, <see langword="false"/> otherwise.</returns>
        public bool Upload(ApplicationTask uploadingPicturesTask, Account account, Picture picture, Action<long> bytesUploadedCallback)
        {
            if (!File.Exists(picture.FileName))
            {
                var errorMessage = string.Format(CultureInfo.CurrentCulture, "The picture to be uploaded does not exist: \"{0}\"", picture.FileName);
                Logger.Log(errorMessage, TraceEventType.Error);
                uploadingPicturesTask.SetError(errorMessage);
                return false;
            }
            else
            {
                lock (this.uploadingLock)
                {
                    try
                    {
                        Logger.Log(string.Format(CultureInfo.CurrentCulture, "Uploading picture: \"{0}\"", picture.FileName), TraceEventType.Information);
                        this.bytesUploadedCallback = bytesUploadedCallback;

                        // Convert some properties to their corresponding Flickr types.
                        var isPublic = picture.VisibilityIsPublic.ValueOr(Picture.VisibilityIsPublicProperty.DefaultValue.ValueOr(true));
                        var isFamily = picture.VisibilityIsFamily.ValueOr(Picture.VisibilityIsFamilyProperty.DefaultValue.ValueOr(true));
                        var isFriend = picture.VisibilityIsFriend.ValueOr(Picture.VisibilityIsFriendProperty.DefaultValue.ValueOr(true));
                        var contentType = (FlickrNet.ContentType)picture.ContentType;
                        var safetyLevel = (SafetyLevel)picture.Safety;

                        // Upload the picture.
                        using (var stream = File.OpenRead(picture.FileName))
                        {
                            using (var handle = new AutoResetEvent(false))
                            {
                                this.flickr.UploadPictureAsync(stream, picture.FileName, picture.Title, picture.Description, picture.Tags, isPublic, isFamily, isFriend, contentType, safetyLevel, HiddenFromSearch.None,
                                    e =>
                                    {
                                        if (e.HasError)
                                        {
                                            var message = string.Format(CultureInfo.CurrentCulture, "An exception occurred during uploading: {0}", e.ErrorMessage);
                                            Logger.Log(message, TraceEventType.Error);
                                            uploadingPicturesTask.SetError(message);
                                        }
                                        else
                                        {
                                            picture.PictureId = e.Result;
                                            handle.Set();
                                        }
                                    }
                                );
                                if (!handle.WaitOne(FlickrClient.uploadTimeout))
                                {
                                    var message = string.Format(CultureInfo.CurrentCulture, "The picture upload did not complete within the timeout period of {0}. However, the upload is not cancelled so the picture might still get uploaded successfully.", FlickrClient.uploadTimeout.ToString());
                                    Logger.Log(message, TraceEventType.Error);
                                    uploadingPicturesTask.SetError(message);
                                }
                            }
                        }

                        // Something went wrong during upload and no picture ID is present; abort.
                        if (string.IsNullOrEmpty(picture.PictureId))
                        {
                            return false;
                        }

                        // Add to sets and groups.
                        foreach (string setId in picture.SetIds)
                        {
                            try
                            {
                                Logger.Log("Adding picture to photoset: " + setId, TraceEventType.Verbose);
                                flickr.PhotosetsAddPhoto(setId, picture.PictureId);
                            }
                            catch (Exception exc)
                            {
                                var errorMessage = string.Format(CultureInfo.CurrentCulture, "Could not add picture \"{0}\" to photo set {1}", picture.ShortFileName, setId);
                                Logger.Log(errorMessage, exc);
                                uploadingPicturesTask.SetError(errorMessage, exc);
                            }
                        }
                        foreach (string groupId in picture.GroupIds)
                        {
                            try
                            {
                                Logger.Log("Adding picture to group: " + groupId, TraceEventType.Verbose);
                                flickr.GroupsPoolsAdd(picture.PictureId, groupId);
                            }
                            catch (Exception exc)
                            {
                                var errorMessage = string.Format(CultureInfo.CurrentCulture, "Could not add picture \"{0}\" to group {1}", picture.ShortFileName, groupId);
                                Logger.Log(errorMessage, exc);
                                uploadingPicturesTask.SetError(errorMessage, exc);
                            }
                        }

                        // Set the license if requested.
                        if (picture.License.HasValue && picture.License != Schedulr.Models.License.None)
                        {
                            try
                            {
                                Logger.Log("Setting license on picture: " + picture.License.Value.ToString(), TraceEventType.Verbose);
                                this.flickr.PhotosLicensesSetLicense(picture.PictureId, (LicenseType)picture.License.Value);
                            }
                            catch (Exception exc)
                            {
                                var errorMessage = string.Format(CultureInfo.CurrentCulture, "Could not set license on picture \"{0}\" to {1}", picture.ShortFileName, picture.License.Value.ToString());
                                Logger.Log(errorMessage, exc);
                                uploadingPicturesTask.SetError(errorMessage, exc);
                            }
                        }

                        // Get some details.
                        var info = flickr.PhotosGetInfo(picture.PictureId);
                        picture.PreviewUrl = info.SmallUrl;
                        picture.WebUrl = info.WebUrl;

                        if (account.Settings.DeleteFileAfterUpload)
                        {
                            File.Delete(picture.FileName);
                            Logger.Log(string.Format(CultureInfo.CurrentCulture, "Deleting file according to settings: \"{0}\"", picture.FileName), TraceEventType.Information);
                        }

                        return true;
                    }
                    finally
                    {
                        this.bytesUploadedCallback = null;
                    }
                }
            }
        }

        /// <summary>
        /// Called when the upload progress changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="FlickrNet.UploadProgressEventArgs"/> instance containing the event data.</param>
        private void OnUploadProgress(object sender, UploadProgressEventArgs e)
        {
            if (this.bytesUploadedCallback != null)
            {
                this.bytesUploadedCallback(e.Bytes);
            }
        }

        #endregion
    }
}